Skip to content

feat(linux): close three parity gaps against the Windows implementation - #32

Merged
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps
Jul 29, 2026
Merged

feat(linux): close three parity gaps against the Windows implementation#32
devhardiyanto merged 4 commits into
mainfrom
linux-parity-gaps

Conversation

@devhardiyanto

Copy link
Copy Markdown
Owner

What

Closes the three real behaviour gaps between linux/phpvm.sh and the Windows implementation: unverified PHP downloads, ext list semantics, and a shallower doctor. Linux/macOS only — Windows is untouched apart from the version bump.

Why

An audit of both implementations found the command surface all but identical. cacert (Windows) and deps (Linux) are the only command-level differences and both are by design. Three differences were not by design, and one of them is a genuine integrity hole.

Splitting phpvm.sh into modules the way windows/src/ is split is queued next — deliberately after this, because a split closes no gap and is safer on top of the behaviour work plus its tests.

How

1. SHA-256 verification of the PHP tarball

The tarball came from php.net and went straight into the build with no check at all, then got cached — so a corrupt or substituted archive would be trusted on every later install too, because a cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli (SHA-512) were already verified in this same file; only PHP itself was not.

The digest now comes from php.net's per-version release JSON and is checked after both the download and the cache path. Mismatch aborts and deletes the file. A missing digest, or a host with no sha256sum/shasum/openssl, degrades to a warning instead of blocking an otherwise valid install — the same fallback Windows takes. PHPVM_SKIP_HASH opts out.

Digest lookup can't reuse the hash_file() trick Composer/wp-cli use: this runs before any PHP exists.

2. ext list gains ON/OFF, ext loaded becomes distinct

Both verbs ran php -m, so one was dead weight and there was no way to see an extension that is available but not enabled. ext list now reports ON/OFF across the union of loaded extensions and the .so files in extension_dir; ext loaded is php -m alone.

ON has to come from php -m rather than a directory listing — extensions compiled into the binary (pdo, mbstring, …) own no .so, and reading the directory alone would drop them.

3. Deeper doctor

  • PATH: only the first php was inspected, so a distro or Homebrew PHP sitting behind phpvm went unreported — the exact situation the check exists for. It now walks PATH and names the second one. (Split via tr, because for d in $PATH does not split on colons in zsh.)
  • extension_dir: checking the directory merely exists passes an ini left pointing at another installed version — precisely what fix-ini repairs. Now compared against PHP_EXTENSION_DIR, mirroring Windows' Test-ExtDirMatch.
  • Which php it reads: it went through PATH, which the check above may have just reported resolves somewhere else. Now goes through the active version's own binary.
  • OpenSSL: the host version is reported upfront, so the OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather than after one. Reported as a note, not a warning, when it cannot be determined.

Changes

  • linux/phpvm.sh_phpvm_php_sha256, _phpvm_sha256_file, _phpvm_verify_tarball; verification wired into phpvm_install after both download and cache paths; phpvm_ext_list rewritten, phpvm_ext_loaded added, ext dispatch de-duplicated; phpvm_doctor checks 2, 3 and a new 6; help text in both phpvm_help and phpvm_ext_help
  • tests/linux/verify.bats — new, 12 tests
  • tests/linux/commands.bats — +18 tests; fake php stub gained FAKE_PHP_INI_EXT_DIR so ini and compiled-in dirs can be driven apart
  • tests/linux/zsh-smoke.zsh — 20 → 40 checks
  • README.md — download verification, ext list semantics, doctor description, PHPVM_SKIP_HASH no longer described as Windows-only
  • Version 1.13.2 → 1.14.0 (5 files + generated windows/phpvm.ps1)

Testing Done

  • bats 82 → 112, all green
  • zsh smoke (zsh 5.9) 20 → 40 checks, all green
  • ShellCheck clean on linux/install.sh + linux/phpvm.sh
  • bash -n and zsh -n clean
  • Windows untouched: drift check OK (15 modules), Pester 168/168 still green
  • Digest parser checked against the live php.net API for 8.3.0, 8.4.1, 7.3.33 and 5.6.40 — picks the .tar.gz digest, never its .bz2/.xz siblings

Two zsh-specific risks are covered by name in the smoke suite, since bats only runs under bash: the PATH scan (colon splitting) and the ext list ON/OFF counters (a while read fed by a here-string, which would have come back as zeroes had it been a pipeline).

An audit of both implementations found the command surface all but
identical - `cacert` (Windows) and `deps` (Linux) are the only differences,
and both are by design. Three gaps were not by design. All three live in
the same file, so they land together.

1. The PHP source tarball was never verified. It was downloaded from
   php.net and handed straight to the build, then cached - so a corrupt or
   substituted archive would be trusted on every later install too, since a
   cached tarball is never re-downloaded. Composer (SHA-384) and wp-cli
   (SHA-512) were already verified here; only PHP itself was not. The
   digest now comes from php.net's per-version release JSON and is checked
   after both the download and the cache path. A mismatch aborts and
   deletes the file. A missing digest, or a host with no
   sha256sum/shasum/openssl, degrades to a warning rather than blocking an
   otherwise valid install - the same fallback Windows takes.
   PHPVM_SKIP_HASH opts out.

2. `ext list` and `ext loaded` both ran `php -m`, so there was no way to
   see an extension that is available but not enabled, and one of the two
   verbs was dead weight. `ext list` now reports ON/OFF over the union of
   loaded extensions and the .so files in extension_dir; `ext loaded` is
   `php -m` alone. ON has to come from `php -m` rather than a directory
   listing, because extensions compiled into the binary own no .so.

3. `doctor` was shallower than its Windows counterpart in three ways.
   It only looked at the first `php` on PATH, so a distro or Homebrew PHP
   waiting behind phpvm went unreported - the very situation the check
   exists for. It checked that extension_dir merely existed, which passes
   an ini left pointing at another installed version, the exact case
   fix-ini repairs. And it read through PATH, which check 2 may have just
   said resolves elsewhere; it now goes through the active version's own
   binary. It also reports the host OpenSSL version upfront, so the
   OpenSSL 3 vs PHP < 8.1 limitation surfaces before a download rather
   than after one.

bats 82 -> 112, zsh smoke 20 -> 40 checks.

devhardiyanto
macOS ships bash 3.2, whose parser chokes on a `case` nested inside a
command substitution - the scan could not even be sourced there, so
phpvm.sh was broken outright on macOS, not merely degraded.

Split PATH with parameter expansion instead. That drops the `$( ... case
... )` construct, and with it the dependency on tr and awk: the check that
tells you PATH is broken should not need to find coreutils on that same
PATH. It also keeps working under zsh, where `for d in $PATH` does not
split on colons.

The PATH tests can now hand doctor a PATH holding nothing but phpvm, so
they stop depending on whether the runner ships a php of its own - which
is what made them pass locally and fail on ubuntu.

devhardiyanto
Those tests hand doctor a PATH holding nothing but phpvm, and bats runs
its own cleanup with whatever PATH the test left behind - so `rm` went
missing and the job exited 1 with all 112 assertions green.

devhardiyanto
@devhardiyanto
devhardiyanto merged commit 08cd6a1 into main Jul 29, 2026
5 checks passed
@devhardiyanto
devhardiyanto deleted the linux-parity-gaps branch July 30, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant